import re
import pprint
from tricks import *
from the import *

def Table(**inits): 
  t=make(inits, A(
      header=[],   keep=False,
      nums=[],     syms=[], scores=[], want=0, 
      classes=[],  rows=[], dists=None, pairs=None))
  t.dists = Relation(lambda x,y: dist0(t.rows[x],t.rows[y]))
  t.pairs = Relation(lambda x,y: mxPlusb(t.rows[x],t.rows[y]))
  return t

def Row(t,cells,row):
  return A(
      _table=t, cells=cells, neighbors=[],cooked=None,row=row)

def Num(**inits): 
  return make(inits, A(
      lo=The.inf, hi=The.ninf, n=0.0, keep=False,w=1,
      unsorted=True, _all=[], mu=0.0, s=0.0, _m2=0.0))

def Sym(**inits): 
  return make(inits, A(
      w=1,counts={}, e=None,most=0, mode=None,n=0))

def _sym():
  x = Sym()
  update1Sym(x,"y")
  return x

def deepCopyNums(nums):
  out = Num(nums.__dict__)
  out._all = nums._all[:]
  return out

def deepCopySyms(syms):
  out = Sym(**syms.__dict__)
  out.counts = syms.counts.copy()
  return out

def addNums(n1,n2) :
  n3 = deepCopyNums(n1)
  all = n2 if isinstance(n2,list) else n2._all
  for n in all:
    update1Num(n,s3)
  return s3

def addSyms(s1,s2) :
  s3 = deepCopySyms(s1)
  print "S2>",s2
  if isinstance(s2,list):
    for one in s2:
      update1Sym(one,s3)
  else:
    for k in s2.counts:
      update1Sym(k,s3,s2.counts[k])
  entropy(s3)
  return s3
               
def updateNums(a,i=None): 
  if not i: i = Num()
  for x in a: 
    update1Num(x,i)
  return i

def updateSyms(a,i=None):
  if not i: i = Sym()
  for x in a: 
    update1Sym(x,i)
  entropy(i)
  return i

def updateNumsX():
 print updateNums([1,2,3,4,4])

def update1Sym(x,i,inc=1):
  if x != The.skip:
    i.e  = None
    i.n += inc
    new  = i.counts[x] = i.counts.get(x,0) + inc
    if new > i.most:
      i.most, i.mode = new,x

def update1Num(x,i):
  if x != The.skip: 
    i.n  += 1
    if i.keep: 
      i.unsorted=True
      i._all += [x]
    if x > i.hi: i.hi = x
    if x < i.lo: i.lo = x
    delta = x - i.mu
    i.mu += delta*1.0/i.n
    i._m2 += delta*(x - i.mu)
    if i.n > 1:
      i.s = (i._m2/(i.n - 1))**0.5

def entropy(sym):
  if not sym.e: 
    sym.e = 0
    for k in sym.counts:
      p  = sym.counts[k] * 1.0 / sym.n
      sym.e += -1 * p * math.log(p, 2)
  return sym.e

def rows(file=None,keep=False):
  t=Table(keep=keep)
  for row,line in enumerate(lines(file)):
    if row == 0:
      width = len(line)
      header(t,line)
    else:
      if len(line) != width:
        oops('#line [',row,'] bad size')
      else:
        cells   = data(t,row,line,[None]*t.want)
        t.rows += [Row(t, cells,row-1)]
  return t 

def clone(t1):
  keep    = t1.keep
  headers = t1.header
  lines   = [x.cells for x in t1.rows]
  t2      = Table(keep = keep)
  header(t2,headers)
  for row,line in enumerate(lines):
    cells    = data(t2,row,line,[None]*t2.want)
    t2.rows += [Row(t2,cells,row)]
  return t2
   
def header(t,line): 
  for col,txt in enumerate(line):
    if not The.skip in txt:
      t.header += [txt]
      what,where = whatWhere(txt,t)
      where  += [what(col=col, at=t.want, name=txt)]
      t.want += 1

def headerX(f="data/weather2.csv"):
  t1 = rows(f)
  t1.pairs=t1.dists=0
  #print t1.header,t1
  t2 = clone(t1)
  t2.pairs=t2.dists=0
  
  #print "\n",t2.header, t2
  print t1.header==t2.header
  print t1.scores
  print t2.scores
  print str(t1.want) == str(t2.want)
  for n,c1 in enumerate(t1.rows):
    c2 = t2.rows[n]
    #print c1,c2
   # if c1 != c2:
     # print n,c1,c2,c1==c2
    c1 = str(c1)
    c2 = str(c2)
    if c1 != c2:
      print ">",n,c1,c2,c1==c2

def whatWhere(x,t):
  if The.num in x:
    return Num,t.scores  if The.goal in x else t.nums
  else:
    return Sym,t.classes if The.goal in x else t.syms 
 
def data(t,row,line,kept):
  for header in contents(t.classes,t.syms):
    x = line[header.col]
    kept[header.at] = x
    update1Sym(x,header)
  for header in contents(t.nums,t.scores):
    x    = line[header.col]
    x,ok = asNum(x)
    if ok:
      kept[header.at] = x
      update1Num(x,header)
    else:
      kept[header.at] = The.skip
      oops('#line [',row,'] bad',header.name,':',x)
  return kept   

class Relation:
  def __init__(i,factory):
    i.cache   = {}
    i.factory = factory
  def has(i,key1,key2):
    if key1 > key2:
      key1,key2 = key2,key1
    if not (key1,key2) in i.cache:
      i.cache[(key1,key2)] = i.factory(key1,key2)
    return i.cache[(key1,key2)]

def dist(r1,r2):
  return r1._table.dists.has(r1.row,r2.row)

def dist0(r1,r2,nums=True,syms=True,
               scores=False,classes=False):
  def sDist(h,x,y): return 0.0 if x == y else 1.0
  def sFar(h,x)   : return "SoMEcrazyTHIing"
  def nDist(h,x,y): return norm(h,x) - norm(h,y)
  def norm(h,x)   : return (x-h.lo)*1.0 / (h.hi-h.lo)
  def nFar(h,x)   : 
    return h.hi if x < (h.hi - h.lo)*0.5 else h.lo    
  ds = ws = 1/The.inf
  if nums: 
    for h in r1._table.nums: 
      ds,ws = dist1(h,ds,ws,r1,r2,nDist,nFar)
  if scores:
    for h in r1._table.scores:
      ds,ws = dist1(h,ds,ws,r1,r2,nDist,nFar)
  if classes:
    for h in r1._table.classes:
      ds,ws = dist1(h,ds,ws,r1,r2,sDist,sFar)
  if syms:
    for h in r1._table.syms: 
      ds,ws = dist1(h,ds,ws,r1,r2,sDist,sFar)
  return ds**0.5 / ws**0.5

def dist1(h,ds,ws,r1,r2,d,far):  
  x = r1.cells[h.at]
  y = r2.cells[h.at]
  nox = x == The.skip
  noy = y == The.skip
  if nox and noy: 
    return ds,ws
  elif x == y: 
    inc = 0.0
  elif not nox and not noy: 
    inc = d(h,x,y)
  elif nox: 
    x = far(h,y)
    inc = d(h,x,y)
  else: 
    y = far(h,x)
    inc = d(h,x,y)
  return ds + h.w*(inc**2), ws + h.w

def furthest(row1,init=-1,gt=lambda x,y: x> y):
  most = init
  for row2 in row1._table.rows:
    delta = dist(row1,row2)
    if gt(delta,most):
      out,most = row2,delta
  return out

def closest(row1):
  return furthest(row1,init=10**32,gt=lambda x,y: x<y)

#sides(t,p[

def Cluster(wheres):
  return A(
    rows=[],wheres=[wheres],free=True)

def neighbors(wheres1,wheres2) :
  for n1,where1 in enumerate(wheres1):
    for n2,where2 in enumerate(wheres2):
        if neighbor(where1,where2):
          return True
  return False

def neighbor(where1,where2):
  for dim,n1 in enumerate(where1):
    if abs(n1 - where2[dim]) > 1:
      return False
  return True

def where(row,pairs,max):
  return tuple([div(x(row,pair),0,1,max) for pair in pairs])

def cube(f,dims=4,chops=4,stop=0.5,minSupport=0.5):
  t = rows(f)
  minSupport = len(t.rows)**minSupport
  pairs = sides(t,dims); print ""
  tiles=Dict(lambda x: Cluster(x))
  for row in t.rows:
    tiles[where(row,pairs,chops)].rows += [row]
  cube1(tiles,stop,minSupport)

def cube1(tiles,stop,minSupport):
  print minSupport
  def size(x)        : return len(tiles[x].rows)
  def ordered(tiles): return sorted(tiles.contents, key=size,reverse=True)
  order = ordered(tiles)
  more=False
  for n1,x1 in enumerate(order):
    if tiles[x1].free: #and size(x1) >= minSupport:
      #print "size",size(x1)
      enough = size(x1)*stop
      buddy=[]
      for n2,x2 in enumerate(order):
        if tiles[x2].free and n2 > n1 and size(x2) >= enough \
              and neighbors(tiles[x1].wheres,
                            tiles[x2].wheres):
          more=True
          tiles[x2].free = False
          tiles[x1].wheres += tiles[x2].wheres
          tiles[x1].rows   += tiles[x2].rows
          buddy += [str(n2)]
      if tiles[x1].free:
        print "After:",n1,tiles[x1].free,tiles[x1].wheres,size(x1), '('+','.join(buddy) + ')'
  if more:
    print ""
    cube1(tiles,stop,minSupport)

def Pair(t, left, right):
  return A(
    left=left, right=right, c= dist(left,right))

# def sides(t,dims) :
#   r1 = one(t.rows)
#   r2 = one(t.rows)
#   while r1.row == r2.row:
#     r2 = one(t.rows)
#   largestEver = dist(r1,r2)
#   return sides1(t,dims-1,r1,r2, largestEver)

# def sides1(t,dims, one, two, largestEver):
#   corners = [one.row, two.row]
#   status  = sides2(t, corners, dims, largestEver)
#   if not status == True:
#       prin(".")
#       return sides1(t,dims, *status)
#   return [Pair(t,t.rows[x],t.rows[y]) 
#           for x,y in pairs(corners)]

def sides(t,dims) :
  r1= one(t.rows)
  r2= furthest(r1)
  r3= furthest(r2)
  largestEver = dist(r2,r3)
  return sides1(t,dims-1,r2,r3,largestEver)

def sides1(t,dims, one, two, largestEver):
  corners = [one.row, two.row]
  status  = sides2(t, corners, dims, largestEver)
  if not status == True:
      prin(".")
      return sides1(t,dims, *status)
  return [Pair(t,t.rows[x],t.rows[y]) 
          for x,y in pairs(corners)]

def x(row, pair):
  a = dist(row, pair.left)
  b = dist(row, pair.right)
  c = pair.c
  return (a**2 + c**2 - b**2) / (2*c + 0.0001)

def sides2(t, corners, dims, largestEver):
  if dims > 0:
    most,furthest = -1, None 
    for x in t.rows:
      if not x.row in corners:
        sum = 0
        for y in [t.rows[id] for id in corners]:
          tmp = dist(x,y)
          if tmp > largestEver:
            return [x,y,tmp]
        sum += tmp
        if sum > most:
          most, furthest = sum, x.row
    if furthest:
      corners += [furthest]
      return sides2(t,corners, dims-1,largestEver)
  return True

@demo
def sideX(f="data/weather2.csv",dims=3):
  "find nearest and furthest neighbor of f rows"
  t = rows(f)
  print "> Seed:",random.random()
  ss = sides(t,dims); print ""
  for s in ss:
    print s.c, s.left.row
  print ss[-1].right.row

@demo
def distX(f="data/weather2.csv"):
  "find nearest and furthest neighbor of f rows"
  t = rows(f)
  for  r1 in t.rows:
    for r2 in t.rows:
      if r2.row > r1.row:
        d = dist(r1,r2)
        r1.neighbors += [(d,r2)]
        r2.neighbors += [(d,r1)]
  log=Abcd()
  for r1 in t.rows:
    r1.neighbors = sorted(r1.neighbors,key=lambda x:x[0])
    r2 = r1.neighbors[0]
    log.keep(r1.cells[-1],r2[1].cells[-1])
    r3 = r1.neighbors[-1]
    print "\n",r1.cells,"\n",r2[1].cells," <== ",\
          r2[0],"\n", r3[1].cells," <== ",r3[0]
  log.header()
  log.report()

class Abcd: 
  def __init__(i,db="all",rx="all"):
    i.db = db; i.rx=rx;
    i.yes = i.no = 0
    i.known = {}; i.a= {}; i.b= {}; i.c= {}; i.d= {}
  def keep(i,actual,predict):
    i.knowns(actual)
    i.knowns(predict)
    if actual == predict: i.yes += 1 
    else                :  i.no += 1
    for x in  i.known:
      if actual == x:
        if  predict == actual: i.d[x] += 1 
        else                 : i.b[x] += 1
      else:
        if  predict == x     : i.c[x] += 1 
        else                 : i.a[x] += 1
  def knowns(i,x):
    if not x in i.known:
      i.known[x]= i.a[x]= i.b[x]= i.c[x]= i.d[x]= 0.0
    i.known[x] += 1
    if (i.known[x] == 1):
      i.a[x] = i.yes + i.no
  def header(i):
    print ('{0:10s} {1:10s}  {2:3s}  {3:3s} {4:3s} '+ \
						 '{5:3s}{6:3s} {7:3s} {8:3s} {9:3s} '+ \
             '{10:3s} {11:3s}{12:3s}{13:10s}').format(i.db, i.rx, \
              "n", "a","b","c","d","acc","pd","pf","prec","f","g","class")
    print '-'*80
  def report(i):
    def p(y) : return int(100*y + 0.5)
    def n(y) : return int(y)
    pd = pf = pn = prec = g = f = acc = 0
    for x in i.known:
      a= i.a[x]; b= i.b[x]; c= i.c[x]; d= i.d[x]
      if (b+d)    : pd   = 1.0*d         / (b+d)
      if (a+c)    : pf   = 1.0*c         / (a+c)
      if (a+c)    : pn   = 1.0*(b+d)     / (a+c)
      if (c+d)    : prec = 1.0*d         / (c+d)
      if (1-pf+pd): g    = 2.0*(1-pf)*pd / (1-pf+pd)
      if (prec+pd): f    = 2.0*prec*pd/(prec+pd)
      if (i.yes + i.no): acc= 1.0*i.yes/(i.yes+i.no)
      print ('{0:10s} {1:10s} {2:3d} {3:3d} {4:3d} '+ \
						 '{5:3d} {6:3d} {7:3d} {8:3d} {9:3d} '+ \
             '{10:3d} {11:3d} {12:3d} {13:10s}').format(i.db, i.rx, \
              (i.yes + i.no), n(a), n(b),n(c), n(d), p(acc), \
              p(pd), p(pf), p(prec), p(f), p(g),x)


####

def rowsX():
  t = rows('data/weather.csv')
  return ":rows %s\n\n:nums %s\n\n:syms %s" % (
    l2s(t.rows),l2s(t.nums),l2s(t.syms))
  
def main():
  if len(sys.argv)==2:
    print eval(sys.argv[1] + '()')
  elif len(sys.argv) > 2 :
    args = [str(asAny(x)) for x in sys.argv[2:]]
    args = '(' + ','.join(args) + ')'
    print eval(sys.argv[1] + args)
  else:
    demo()
#len(sys.argv) > 2:
 #   args = sys.argv[2:]
  #  print eval(sys.argv[1] + '(' + ','.join
   # demo()


def profile(): 
  import cProfile;cProfile.run('rows("data/autompg50K.csv")')

main()
